﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Collections;
using System.Xml;
using MvcEngine.Core.Utils;
using System.ComponentModel;
using System.Xml.Linq;
using System.IO;

namespace MvcEngine.Core.Base
{
    /// <summary>
    /// Represents a collection of custom fields.
    /// </summary>
    public class FieldsDictionary : IEnumerable<EntityField>, IXmlSerializable
    {
        public static implicit operator FieldsDictionary(string xml)
        {
            if (string.IsNullOrEmpty(xml))
                return new FieldsDictionary();
            return XmlUtil.DeserializeObject(xml, typeof(FieldsDictionary)) as FieldsDictionary;
        }

        public static implicit operator string(FieldsDictionary fields)
        {
            return XmlUtil.SerializeObject(fields);
        }

        #region Private fields

        private Dictionary<string, EntityField> fields = new Dictionary<string, EntityField>();
        private bool isDirty = false;

        #endregion

        /// <summary>
        /// Gets or sets a value indicating whether this instance was modified.
        /// </summary>
        /// <value><c>true</c> if this instance was modified; otherwise, <c>false</c>.</value>
        public bool IsDirty
        {
            get { return isDirty; }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FieldsDictionary"/> class.
        /// </summary>
        public FieldsDictionary()
        {
        }

        /// <summary>
        ///Adds the specified key and value to the dictionary.
        /// </summary>
        /// <param name="value">The value of the element to add. The value can be null for reference types.</param>
        public void Add(EntityField value)
        {
            value.Changed += FieldChangedEventHandler;
            fields.Add(value.Name, value);
            isDirty = true;
        }

        /// <summary>
        /// Adds the specified item. Used when deserializing an object.
        /// </summary>
        /// <param name="item">The item.</param>
        public void Add(object item)
        {
            var i = (EntityField)item;
            Add(i);
        }

        /// <summary>
        /// Determines whether the <see cref="FieldsDictionary"/> contains the specified key.
        /// </summary>
        /// <param name="key">The key to locate in the <see cref="FieldsDictionary"/>.</param>
        /// <returns>
        /// 	<c>true</c> true if the <see cref="FieldsDictionary"/> contains an element with the specified key; otherwise, <c>false</c>.
        /// </returns>
        public bool ContainsKey(string key)
        {
            return fields.ContainsKey(key);
        }

        /// <summary>
        /// Gets a collection containing the keys of the <see cref="FieldsDictionary"/>.
        /// </summary>
        /// <value>The keys.</value>
        public ICollection<string> Keys
        {
            get { return fields.Keys; }
        }

        /// <summary>
        /// Removes the value with the specified key.
        /// </summary>
        /// <param name="key">The key of the element to remove.</param>
        /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
        public bool Remove(string key)
        {
            isDirty = true;
            EntityField item = null;
            if (fields.TryGetValue(key, out item))
            {
                item.Changed -= FieldChangedEventHandler;
                return fields.Remove(key);
            }
            return false;
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key of the value to get.</param>
        /// <param name="value">When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.</param>
        /// <returns> <c>true</c> if the <see cref="FieldsDictionary"/> contains an element with the specified key; otherwise, <c>false</c>.</returns>
        public bool TryGetValue(string key, out EntityField value)
        {
            return fields.TryGetValue(key, out value);
        }

        /// <summary>
        /// Gets a collection containing the values in the <see cref="FieldsDictionary"/>.
        /// </summary>
        /// <value>The values.</value>
        public ICollection<EntityField> Values
        {
            get { return fields.Values; }
        }

        /// <summary>
        /// Gets or sets the value associated with the specified key.
        /// </summary>
        /// <value>The value associated with the specified key. If the specified key is not found, a get operation throws a System.Collections.Generic.KeyNotFoundException, and a set operation creates a new element with the specified key.</value>
        public EntityField this[string key]
        {
            get
            {
                return fields[key];
            }
            set
            {
                if (!fields.ContainsKey(key))
                    Add(value);
                else
                    fields[key] = value;
                isDirty = true;
            }
        }

        /// <summary>
        /// Removes all keys and values from the <see cref="FieldsDictionary"/>.
        /// </summary>
        public void Clear()
        {
            isDirty = true;
            foreach (var item in fields.Values)
                item.Changed -= FieldChangedEventHandler;
            fields.Clear();
        }

        /// <summary>
        /// Gets the number of key/value pairs contained in the <see cref="FieldsDictionary"/>.
        /// </summary>
        /// <value>The number of key/value pairs contained in the <see cref="FieldsDictionary"/></value>
        public int Count
        {
            get { return fields.Count; }
        }

        /// <summary>
        /// Copies all entries from the current dictionary to the specified one.
        /// </summary>
        /// <param name="other">The other.</param>
        public void CopyTo(FieldsDictionary other)
        {
            foreach (var field in this)
            {
                EntityField otherField = new EntityField() { Name = field.Name, Value = field.Value };
                foreach (var item in field.GetAttributes())
                    otherField.Set(item.Key, item.Value);
                other.Remove(otherField.Name);
                other.Add(otherField);
            }
        }

        /// <summary>
        /// Safely gets the value by name. If there is no field with the specified name, returns <see langword="null"/>.
        /// </summary>
        /// <param name="name">The name of the field.</param>
        /// <returns>Returns the field value if the field exists; otherwise returns <see langword="null"/>.</returns>
        public object GetValue(string name)
        {
            EntityField field;
            if (fields.TryGetValue(name, out field))
                return field.Value;
            return null;
        }   

        /// <summary>
        /// Sets the value for the field with the specified name.
        /// </summary>
        /// <param name="name">The name of the field.</param>
        /// <param name="value">The value.</param>
        public void SetValue(string name, object value)
        {
            SetField(new EntityField() { Name = name, Value = value });
        }

        /// <summary>
        /// Sets the field with the specified name. If the field does exist it is replaced by the specified one.
        /// </summary>
        /// <param name="field">The field.</param>
        public void SetField(EntityField field)
        {
            Remove(field.Name);
            Add(field);
        }

        private void FieldChangedEventHandler(object sender, EventArgs e)
        {
            this.isDirty = true;
        }

        #region IEnumerable<EntityField> Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<EntityField> GetEnumerator()
        {
            return fields.Values.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion

        #region IXmlSerializable Members

        /// <summary>
        /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> to the class.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema"/> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/> method.
        /// </returns>
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> stream from which the object is deserialized.</param>
        public void ReadXml(XmlReader reader)
        {
            if (!reader.IsEmptyElement)
            {
                if (!reader.Read())
                    return;
                while (reader.NodeType != XmlNodeType.EndElement)
                {
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        string name = reader.Name;
                        reader.MoveToFirstAttribute();
                        string type = reader.Value;
                        EntityField field = new EntityField() { Name = name };
                        while (reader.MoveToNextAttribute())
                        {
                            field[reader.Name] = reader.Value;
                        }
                        reader.MoveToElement();
                        if (type == "Null")
                            reader.Read(); // Move to next element. The field value is default to null.
                        else
                            field.Value = XmlUtil.DeserializeObject(reader.ReadInnerXml(), Type.GetType(type));
                        this.Add(field);
                    }
                }
            }
            reader.Read();
            isDirty = false;
        }

        /// <summary>
        /// Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter"/> stream to which the object is serialized.</param>
        public void WriteXml(XmlWriter writer)
        {
            foreach (var item in this.Values)
            {
                SerializeField(writer, item);
            }
        }

        private void SerializeField(XmlWriter writer, EntityField field)
        {
            writer.WriteStartElement(field.Name);
            string assemblyName = "Null";

            if (field.Value != null)
            {
                Type type = field.Value.GetType();
                assemblyName = type.ToString() + ", " + type.Assembly.GetName().Name;
            }

            writer.WriteAttributeString("type", assemblyName);
            foreach (var item in field.GetAttributes())
                writer.WriteAttributeString(item.Key, item.Value);
            if (field.Value != null)
            {
                string xml = XmlUtil.SerializeObject(field.Value);
                xml = System.Text.RegularExpressions.Regex.Replace(xml, @"\<\?xml.+\?\>", string.Empty,
                    System.Text.RegularExpressions.RegexOptions.Compiled);
                writer.WriteRaw(xml);
            }
            writer.WriteEndElement();
        }

        #endregion
    }
}
